/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once

#include <baja/common.hpp>
#include <baja/vars.hpp>

namespace baja {

const unsigned int varFlagReadOnly = 0x01;

namespace vars {

// globals
BAJA_BEGIN_GLOBALS(globals)
    BAJA_DECLARE_CONST_GLOBAL(wchar*, trueString)
    BAJA_DECLARE_CONST_GLOBAL(wchar*, falseString)
BAJA_END_GLOBALS(globals)

BAJA_DEFINE_CONST_GLOBAL(globals, wchar*, trueString, L"true")
BAJA_DEFINE_CONST_GLOBAL(globals, wchar*, falseString, L"false")

} // namespace vars

template<typename T> class binary_var_wrapper : public ivar_wrapper
{
public:
    binary_var_wrapper() : _data(nullptr) {}

    void initialize(
        const wchar* name,
        const var_type type,
        T* data,
        const T trueValue,
        const T falseValue,
        const uint32 flags
        )
    {
        BAJA_CHECK_ARG(name);
        BAJA_CHECK_ARG(type == var_type::typeBool);
        BAJA_CHECK_ARG(data);

        _name = name;
        _type = type;
        _data = data;
        _trueValue = trueValue;
        _falseValue = falseValue;
        _flags = flags;
        _readOnly = ((flags & varFlagReadOnly) == varFlagReadOnly);
    }

    void increment()
    {
        if (!_readOnly && _data != nullptr)
        {
            *_data = _trueValue;
        }
    }

    void incrementAndWrap()
    {
        if (!_readOnly && _data != nullptr)
        {
            *_data = (*_data == _trueValue) ? 
                _falseValue : _trueValue;
        }
    }

    void decrement()
    {
        if (!_readOnly && _data != nullptr)
        {
            *_data = _falseValue;
        }
    }

    std::wstring getName()
    {
        return _name;
    }

    var_type getType()
    {
        return _type;
    }

    uint32 getFlags()
    {
        return _flags;
    }

    void* getData()
    {
        return reinterpret_cast<void*>(_data);
    }

    std::wstring getDataAsString()
    {
        if (*_data == _trueValue)
        {
            return vars::globals::trueString;
        }
        else
        {
            return vars::globals::falseString;
        }
    }

private:
    std::wstring _name;
    var_type _type;
    T* _data;
    T _trueValue;
    T _falseValue;
    uint32 _flags;
    bool _readOnly;
};

template<typename T> class ranged_var_wrapper : public ivar_wrapper
{
public:
    ranged_var_wrapper() : _data(nullptr) {}

    void initialize(
        const wchar* name,
        const var_type type,
        T* data,
        const T minValue,
        const T maxValue,
        const T step,
        const uint32 flags
        )
    {
        BAJA_CHECK_ARG(name);
        BAJA_CHECK_ARG(type != var_type::typeBool);
        BAJA_CHECK_ARG(type != var_type::typeString);
        BAJA_CHECK_ARG(type != var_type::typeEnum);
        BAJA_CHECK_ARG(data);

        _name = name;

        _type = type;
        _data = data;
        _minValue = minValue;
        _maxValue = maxValue;
        _step = step;
        _flags = flags;
        _readOnly = ((flags & varFlagReadOnly) == varFlagReadOnly);
    }

    void increment()
    {
        if (!_readOnly && _data != nullptr)
        {
            T newValue = *_data + _step;

            // catch overflow conditions
            if (newValue < _minValue || newValue > _maxValue)
            {
                newValue = _maxValue;
            }

            *_data = newValue;
        }
    }

    void incrementAndWrap()
    {
        if (!_readOnly && _data != nullptr)
        {
            T newValue = *_data + _step;

            // catch overflow conditions
            if (newValue < _minValue || newValue > _maxValue)
            {
                newValue = _minValue;
            }

            *_data = newValue;
        }
    }

    void decrement()
    {
        if (!_readOnly && _data != nullptr)
        {
            T newValue = *_data - _step;

            // catch overflow conditions
            if (newValue > _maxValue || newValue < _minValue)
            {
                newValue = _minValue;
            }

            *_data = newValue;
        }
    }

    std::wstring getName()
    {
        return _name;
    }

    var_type getType()
    {
        return _type;
    }

    uint32 getFlags()
    {
        return _flags;
    }

    void* getData()
    {
        return reinterpret_cast<void*>(_data);
    }

    std::wstring getDataAsString()
    {
        std::wstring result;
        switch (_type)
        {
            case var_type::typeInt:
            {
                result = (boost::wformat(L"%d") % (int)*_data).str();
                break;
            }

            case var_type::typeUint:
            {
                result = (boost::wformat(L"%u") % (int)*_data).str();
                break;
            }

            case var_type::typeFloat:
            {
                result = (boost::wformat(L"%0.08f") % (float32)*_data).str();
                break;
            }
        }

        return result;
    }

private:
    std::wstring _name;
    var_type _type;
    T* _data;
    T _minValue;
    T _maxValue;
    T _step;
    uint32 _flags;
    bool _readOnly;
};

class string_var_wrapper : public ivar_wrapper
{
public:
    string_var_wrapper() : _data(nullptr) {}

    void initialize(
        const wchar* name,
        wchar* data,
        const uint32 flags
        )
    {
        BAJA_CHECK_ARG(name);
        BAJA_CHECK_ARG(data);

        _name = name;

        _data = data;
    }

    void increment()
    {
    }

    void incrementAndWrap()
    {
    }

    void decrement()
    {
    }

    std::wstring getName()
    {
        return _name;
    }

    var_type getType()
    {
        return var_type::typeString;
    }

    uint32 getFlags()
    {
        return varFlagReadOnly;
    }

    void* getData()
    {
        return reinterpret_cast<void*>(_data);
    }

    std::wstring getDataAsString()
    {
        return _data;
    }

private:
    std::wstring _name;
    wchar* _data;
};

class enum_var_wrapper : public ivar_wrapper
{
public:
    enum_var_wrapper() : _data(nullptr), _currentValue(0) {}

    void initialize(
        const wchar* name,
        int* data,
        const int* enumValues,
        const wchar** enumNames,
        const uint32 totalValues,
        const uint32 flags
        )
    {
        BAJA_CHECK_ARG(name);
        BAJA_CHECK_ARG(data);
        BAJA_CHECK_ARG(enumValues);
        BAJA_CHECK_ARG(enumNames);
        BAJA_CHECK_ARG(totalValues > 0);

        _name = name;

        _enumValues.resize(totalValues);
        _enumNames.resize(totalValues);
        for (uint32 i = 0; i < totalValues; i++)
        {
            _enumValues[i] = enumValues[i];
            _enumNames[i] = std::wstring(enumNames[i]);
        }

        for (uint32 i = 0; i < totalValues; i++)
        {
            if (*data == enumValues[i])
            {
                _currentValue = i;
            }
        }

        _data = data;
        _flags = flags;
        _readOnly = ((flags & varFlagReadOnly) == varFlagReadOnly);
    }

    void increment()
    {
        if (!_readOnly && _data != nullptr)
        {
            _currentValue++;
            if (_currentValue >= _enumNames.size())
            {
                _currentValue = _enumNames.size() - 1;
            }

            *_data = _enumValues[_currentValue];
        }
    }

    void incrementAndWrap()
    {
        if (!_readOnly && _data != nullptr)
        {
            _currentValue++;
            if (_currentValue >= _enumNames.size())
            {
                _currentValue = 0;
            }

            *_data = _enumValues[_currentValue];
        }
    }

    void decrement()
    {
        if (!_readOnly && _data != nullptr)
        {
            if (_currentValue > 0)
            {
                _currentValue--;
            }

            *_data = _enumValues[_currentValue];
        }
    }

    std::wstring getName()
    {
        return _name;
    }

    var_type getType()
    {
        return var_type::typeEnum;
    }

    uint32 getFlags()
    {
        return _flags;
    }

    void* getData()
    {
        return reinterpret_cast<void*>(_data);
    }

    std::wstring getDataAsString()
    {
        // make sure we update _currentValue in case content of _data got updated
        // without us knowing.
        size_t totalValues = _enumValues.size();
        for (size_t i = 0; i < totalValues; i++)
        {
            if (*_data == _enumValues[i])
            {
                _currentValue = i;
            }
        }
        return _enumNames[_currentValue];
    }

private:
    std::wstring _name;
    int* _data;
    std::vector<int> _enumValues;
    std::vector<std::wstring> _enumNames;
    uint32 _flags;
    bool _readOnly;
    size_t _currentValue;
};

class var_group : public ivar_group, public std::enable_shared_from_this<ivar_group>
{
public:
    var_group() {}

    static std::shared_ptr<var_group> create(const wchar* name)
    {
        std::shared_ptr<var_group> returnValue = std::make_shared<var_group>();
        returnValue->initialize(name);
        return returnValue;
    }
    
    void initialize(const wchar* name)
    {
        _name = name;
    }

    std::wstring getName()
    {
        return _name;
    }

    std::shared_ptr<ivar_group> getParent()
    {
        return _parent.lock();
    }

    void addChildGroup(const std::shared_ptr<ivar_group>& group)
    {
        // get the groups current parent
        std::shared_ptr<ivar_group> parentGroup = group->getParent();

        // continue only if parent is different
        if (this != parentGroup.get())
        {
            // if group had a parent, remove it from that parent group
            if (parentGroup.get() != nullptr)
            {
                parentGroup->removeChildGroup(group);
            }

            // now add to this group
            _groups.push_back(group);

            // set parent of the new child
            std::shared_ptr<var_group> childGroup = std::dynamic_pointer_cast<var_group>(group);
            if (childGroup.get() != nullptr)
            {
                childGroup->_parent = shared_from_this();
            }
        }
    }

    void removeChildGroup(const std::shared_ptr<ivar_group>& group)
    {
        // find the group
        for (uint32 i = 0; i < _groups.size(); i++)
        {
            if (_groups[i].get() == group.get())
            {
                // we found the group - clear it's parent
                std::shared_ptr<var_group> childGroup = std::dynamic_pointer_cast<var_group>(group);
                if (childGroup.get() != nullptr)
                {
                    childGroup->_parent.reset();
                }

                // remove the child group from our array
                _groups.erase(_groups.begin() + i);
                break;
            }
        }
    }

    size_t getChildGroupCount()
    {
        return _groups.size();
    }

    std::shared_ptr<ivar_group> getChildGroup(const uint32 index)
    {
        return _groups[index];
    }

    void addBoolVar(
        const wchar* name,
        bool* data,
        const uint32 flags
        )
    {
        std::shared_ptr<binary_var_wrapper<bool>> var = std::make_shared<binary_var_wrapper<bool>>();

        var->initialize(
            name,
            var_type::typeBool,
            data,
            true,
            false,
            flags
            );

        _vars.push_back(var);
    }

    void addIntVar(
        const wchar* name,
        int* data,
        const int minValue,
        const int maxValue,
        const int changeStep,
        const uint32 flags
        )
    {
        std::shared_ptr<ranged_var_wrapper<int>> var = std::make_shared<ranged_var_wrapper<int>>();
            
        var->initialize(
            name,
            var_type::typeInt,
            data,
            minValue,
            maxValue,
            changeStep,
            flags
            );

        _vars.push_back(var);
    }

    void addUintVar(
        const wchar* name,
        uint32* data,
        const uint32 minValue,
        const uint32 maxValue,
        const uint32 changeStep,
        const uint32 flags
        )
    {
        std::shared_ptr<ranged_var_wrapper<uint32>> var = std::make_shared<ranged_var_wrapper<uint32>>(); 
        var->initialize(
            name,
            var_type::typeUint,
            data,
            minValue,
            maxValue,
            changeStep,
            flags
            );
        _vars.push_back(var);
    }

    void addFloatVar(
        const wchar* name,
        float32* data,
        const float32 minValue,
        const float32 maxValue,
        const float32 changeStep,
        const uint32 flags
        )
    {
        std::shared_ptr<ranged_var_wrapper<float32>> var = std::make_shared<ranged_var_wrapper<float32>>(); 
        
        var->initialize(
            name,
            var_type::typeFloat,
            data,
            minValue,
            maxValue,
            changeStep,
            flags
            );
        _vars.push_back(var);
    }

    void addStringVar(
        const wchar* name,
        wchar* data,
        const uint32 flags
        )
    {
        std::shared_ptr<string_var_wrapper> var = std::make_shared<string_var_wrapper>();
        var->initialize(
            name,
            data,
            flags
            );

        _vars.push_back(var);
    }

    void addEnumVar(
        const wchar* name,
        int* data,
        const int* enumValues,
        const wchar** enumNames,
        const uint32 totalValues,
        const uint32 flags
        )
    {
        std::shared_ptr<enum_var_wrapper> var = std::make_shared<enum_var_wrapper>();
        var->initialize(
            name,
            data,
            enumValues,
            enumNames,
            totalValues,
            flags
            );
        _vars.push_back(var);
    }

    size_t getVarCount()
    {
        return _vars.size();
    }

    std::shared_ptr<ivar_wrapper> getVar(const size_t index)
    {
        BAJA_CHECK_ARG(index < _vars.size());

        return _vars[index];
    }

private:
    std::vector<std::shared_ptr<ivar_wrapper>> _vars;
    std::vector<std::shared_ptr<ivar_group>> _groups;
    std::weak_ptr<ivar_group> _parent;
    std::wstring _name;
};

inline std::shared_ptr<ivar_group> createVarGroup(const wchar* name)
{
    return std::dynamic_pointer_cast<ivar_group>(var_group::create(name));
}

} // namespace baja
